package org.upfrost;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import javax.sql.DataSource;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.upfrost.cache.Cache;
import org.upfrost.util.Pair;
import org.upfrost.util.cache.GenericCacheStats;

/**
 * The session is the layer that provides an access to the database. It manages a connection to the database, 
 * instanciates Queries and transactions. It usually lives a short life, but there's no specific limit to its lifespan.
 * The main use of the session is to create Query instances that actually do the real work.
 * <p/>
 * MapperSession objects are typically bound to a thread, and they keep track of all the Query instances created by createQuery() calls.
 * The close() method must be called when the session is not used anymore, it closes all the queries and connection opened by the session.
 *  
 * @author Rodrigo Reyes
 *
 */
public class MapperSession 
{
	static private Log log = LogFactory.getLog(MapperSession.class);

	private Mapper m_mapper;
	private DataSource m_dataSource;
	private Connection m_connection = null;
	private LinkedList<Query> m_queries = null;
	private Date m_creationTime = new Date();
	private CloseListener m_closeListener = null;
	private boolean m_autoCommit = true;

	private String m_name;

    public boolean m_cacheEnabled = true;
    
    public Stack<State> m_savedState = new Stack<State>();
    
	public static interface CloseListener
	{
		public void closedSession(MapperSession session);
	}

	private class State
	{
		boolean autoCommit;
		boolean useCache;
	}

	/**
	 * Create a new session. Don't use it directly, use the Mapper to initialize new sessions.
	 * @param mapper
	 * @param dataSource
	 * @param close
	 * @param name
	 */
	public MapperSession(Mapper mapper, DataSource dataSource, CloseListener close, String name)
	{
		m_mapper = mapper;
		m_dataSource = dataSource;
		m_closeListener = close;
		m_name = name;
	}

	synchronized protected Connection getConnection()
	{
		if (m_connection == null)
		{
			try {
				m_connection = m_dataSource.getConnection();
				m_connection.setAutoCommit(m_autoCommit);
			} catch (SQLException e) {
				throw new RuntimeException("Can't get a new connection from datasource", e);
			}
		}
		return m_connection;
	}

	/**
	 * Retrieve the metadata for the database this session is connected to.
	 * @return a DatabaseMetadata instance
	 * @throws SQLException if for any reason the metadata failed to be retrieved
	 */
	public DatabaseMetaData getMetaData() throws SQLException
	{
		return getConnection().getMetaData();
	}

	@Override
	protected void finalize() throws Throwable {
		close();
		super.finalize();
	}

	/**
	 * Configure the autocommit property of connections used by this session 
	 * @param b true to enable the autocommit, false otherwise.
	 * @throws SQLException
	 */
	public void setAutoCommit(boolean b) throws SQLException
	{
		m_autoCommit = b;
		if (m_connection != null)
		{
			m_connection.setAutoCommit(b);
		}
	}

	/**
	 * Get the autocommit property of this session. Default is true.
	 * @return the state of the autocommit
	 */
	public boolean getAutoCommit()
	{
		return m_autoCommit;
	}

	/**
	 * Create a new Query 
	 * @param query the SQL query to create a Query instance for
	 * @return a Query
	 */
	public Query<?> createQuery(String query)
	{
		return this.createQuery(query, (ICacheManager)null);
	}
	
	/**
	 * Create a new Query
	 * @param query query the SQL query to create a Query instance for
	 * @param cache a cache manager, of null
	 * @return a Query
	 */
	public Query createQuery(String query, ICacheManager cache)
	{
		// force cache to be null if cache is disabled
		if (m_cacheEnabled == false)
			cache = null;
		
		Query q = new Query(m_mapper, getConnection(), query, cache);
		if (m_queries == null)
			m_queries = new LinkedList<Query>();
		m_queries.add(q);
		m_mapper.getStatistics().incCreatedQuery();
		return q;
	}

	public void freeQueries()
	{
		if (m_queries != null)
		{
			for (Query q: m_queries)
			{
				try {
					q.close();
				} catch (Exception exc)
				{
					exc.printStackTrace();
				}
			}
			m_queries = null;
		}
	}
	
	/**
	 * Close the session, and all the queries and connection opened during the lifespan of the session.
	 */
	synchronized public void close()
	{
		freeQueries();
		try {
			if (m_connection != null)
			{
				m_connection.close();
				m_connection = null;
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		m_closeListener.closedSession(this);
	}

	/**
	 * The creation date of this session.
	 * @return a date
	 */
	public Date getCreationTime()
	{
		return m_creationTime;
	}

	/**
	 * If the session autocommit is set to false, this commits the changes.
	 * @throws SQLException
	 */
	public void commit() throws SQLException
	{
		getConnection().commit();
	}

	/**
	 * If the session autocommit is set to false, calling this method cancels the updates created since the last call to commit or rollback, or the creation of the session.
	 * @throws SQLException
	 */
	public void rollback() throws SQLException
	{
		getConnection().rollback();
	}

	/**
	 * Get the Mapper instance that created this session.
	 * @return a Mapper
	 */
	public Mapper getMapper() {
		return m_mapper;
	}

	@Override
	public String toString() 
	{
		return "[Session:" + m_name + ":" + (m_queries!=null?m_queries.size():0)+"]";
	}	

	/**
	 * Read the global cache of the Mapper
	 * 
	 * @param dao the DAO object which cache we want to read
	 * @param query the query
	 * @param params the parameters of the query
	 * @return an Object, which can be null, or Cache.NOT_FOUND if the tuple (query,params) is not cached.
	 */
	public Object readCache(AbstractDAO<?> dao, String query, Object...params)
	{
		if (m_cacheEnabled)
			return getCache().read(dao, query, params);
		else
			return Cache.NOT_FOUND;
	}
	
	/**
	 * Write to the global cache of the Mapper, for a DAO object and the tuple (query,params).
	 * 
	 * @param dao the DAO object which cache we want to read
	 * @param result the result of the query
	 * @param query the query
	 * @param params the parameters of the query
	 */
	public void writeCache(AbstractDAO<?> dao, Object result, String query, Object...params)
	{
		if (m_cacheEnabled)
			getCache().write(dao, result, query, params);
	}
	
	/**
	 * Clear the cache of a DAO class
	 * @param dao the DAO to clear the cache for
	 */
	public void clearCache(AbstractDAO<?> dao)
	{
		if (getCache() != null)
			getCache().clear(dao);
	}

	/**
	 * Return the active state of the cache for this session
	 * @return true if the cache is active, false if it is disabled
	 */
	public boolean isCacheEnabled() {
		return m_cacheEnabled;
	}

	/**
	 * If the cache is disabled, nothing is read or written into/from the cache.
	 * 
	 * @param cacheEnabled true to enable the cache, false to disable it.
	 */
	public void setCacheEnabled(boolean cacheEnabled) {
		m_cacheEnabled = cacheEnabled;
	}

	/**
	 * Save the configuration of the session, and push it on a stack. It's 
	 * legit to call this method several times, each time the current configuration is pushed on a stack, 
	 * and can be retrieved with a restoreState() called.  
	 * @return the MapperSession itself
	 */
	public MapperSession saveState()
	{
		State s = new State();
		s.autoCommit = this.m_autoCommit;
		s.useCache = this.m_cacheEnabled;
		m_savedState.push(s);
		
		return this;
	}
	
	/**
	 * Restore the configuration of the session, as saved by a previous saveState() call.
	 * @return the MapperSession itself
	 */
	public MapperSession restoreState()
	{
		State s = m_savedState.pop();
		this.m_autoCommit = s.autoCommit;
		this.m_cacheEnabled = s.useCache;

		return this;
	}

	/**
	 * Get the Statistics for the global cache
	 * @return
	 */
	public List<Pair<Class<?>,GenericCacheStats>> getStats()
	{
		return getCache().getStats();
	}

	/**
	 * Get the global results cache
	 * @return the cache
	 */
	public Cache getCache()
	{
		return this.getMapper().getCache();
	}
	
}
